// This is a header only library.
//
// Use macro NOSCRIPT_IMPL to add implementation into your source tree.
//
//   #define NOSCRIPT_IMPL
//   #include "noscript.hpp"
//
// How to use for maintainers.
// 1. Put "noscript.hpp" to root of source tree.
// 2. Add "noscript.cpp" to include "noscript.hpp" with "NOSCRIPT_IMPL".
// 3. Call "noscript::RebuildNoScript" in main function.
// 4. Call other functions to build the project after.
//
// How to use for developers.
// 1. Build "noscript.cpp" to "noscript" with a C++ compiler.
// 2. Run ./noscript to build the project.

#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// C++
#include <cassert>
#include <cerrno>
#include <string>
#include <vector>

// POSIX
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

#include <cstdarg>
#include <ctime>

#include "libnoscript.hpp"

namespace noscript {

//
// Logging
//

LogLevel MIN_LOG_LEVEL = LOG_INFO;

#define LOG(...) NOSCRIPT_LOG(__VA_ARGS__)
#define LOG_IS_ON(...) NOSCRIPT_LOG_IS_ON(__VA_ARGS__)
#define LOG_CTX(...) NOSCRIPT_LOG_CTX(__VA_ARGS__)

LogCtx::LogCtx(FILE* fp, char const* file, int line, LogLevel level)
    : fp_(fp)
{
    static char const* name[] = { "DEBUG", "INFO ", "ERROR" };
    static char const* color[] = {
        "", // LOG_DEBUG
        kColorBoldWhite, // LOG_INFO
        kColorRed, // LOG_ERROR
    };

#if NOSCRIPT_IS_ON(NOSCRIPT_DEBUG)
    fprintf(fp_, "%s[NOSCRIPT (%d) %s %s:%d] ",
        color[level], (int)Pid::Instance().Get(), name[level], file, line);
#else
    (void)file;
    (void)line;
    fprintf(fp_, "%s[NOSCRIPT (%d) %s] ",
        color[level], (int)Pid::Instance().Get(), name[level]);
#endif
}

//
// Command
//

Cmd::~Cmd() { }

// 子进程返回 ERR_CHILD，父进程返回 ERR_OK。
//
// 子进程的 Pid::Instance 会得到更新。
static Error Fork(pid_t* pid)
{
    *pid = fork();
    if (*pid < 0) {
        LOG(ERROR, "fork: %s", strerror(errno));
        return ERR_OOPS;
    }

    if (*pid == 0) {
        Pid::Instance().Sync();
        return ERR_CHILD;
    }

    return ERR_OK;
}

ChildOutput Cmd::GetOutput()
{
    int pipe_fd[2];
    pid_t pid;
    std::vector<char*> argv;
    char buffer[1024];
    ssize_t count;
    std::string result;

    if (pipe(pipe_fd) != 0) {
        LOG(ERROR, "failed to create pipe: %s", strerror(errno));
        return ChildOutput(ERR_OOPS, 0);
    }

    switch (Fork(&pid)) {
    case ERR_CHILD:
        close(pipe_fd[0]); // 关闭读取端

        // 子进程的 stdout 重定向到管道的写端。
        dup2(pipe_fd[1], STDOUT_FILENO);
        close(pipe_fd[1]);

        for (usize i = 0; i < argv_.size(); ++i) {
            argv.push_back(&argv_[i][0]);
        }
        argv.push_back(NULL);

        execvp(argv[0], argv.data());

        LOG(ERROR, "execvp: %s", strerror(errno));

        // 退出子进程，而不是返回错误。
        exit(1);

    case ERR_OK:
        close(pipe_fd[1]); // 关闭写入端

        while ((count = read(pipe_fd[0], buffer, sizeof(buffer))) > 0) {
            result.append(buffer, count);
        }

        close(pipe_fd[0]);

        int status;
        waitpid(pid, &status, 0);

        if (status == 0) {
            return ChildOutput(result, "");
        } else {
            return ChildOutput(ERR_CHILD, status);
        }

    default:
        return ChildOutput(ERR_OOPS, 0);
    }
}

Child Cmd::Spawn()
{
    pid_t pid;
    switch (Fork(&pid)) {
    case ERR_CHILD:
        RunChild();
        return Child(ERR_OK, 0);

    case ERR_OK:
        argv_.clear();
        return Child(ERR_OK, pid);

    default:
        return Child(ERR_OOPS, 0);
    }
}

void Cmd::RunChild()
{
    Pid::Instance().Sync();

    do {
        if (LOG_IS_ON(INFO))
            break;

        LogCtx ctx = LOG_CTX(INFO);

        ctx.Printf("$");
        for (usize i = 0; i < argv_.size(); ++i) {
            ctx.Printf(" %s", argv_[i].c_str());
        }
    } while (false);

    std::vector<char*> argv;
    for (usize i = 0; i < argv_.size(); ++i) {
        argv.push_back(&argv_[i][0]);
    }
    argv.push_back(NULL);

    execvp(argv[0], argv.data());

    LOG(ERROR, "execvp: %s", strerror(errno));

    // 退出子进程，而不是返回错误。
    exit(1);
}

Error Cmd::Run()
{
    Child handle = Spawn();

    if (!handle.IsOk())
        return handle.GetError();

    return handle.Join();
}

Error Child::Join()
{
    if (!IsOk()) {
        LOG(ERROR, "try to join a CmdHandle with error");
        return GetError();
    }

    int status;
    if (waitpid(pid_, &status, 0) < 0) {
        LOG(ERROR, "failed to wait child with pid: %d", (int)pid_);
        return ERR_OOPS;
    }

    if (status != 0) {
        LOG(ERROR, "pid %d report error: %d", (int)pid_, status);
        return ERR_OOPS;
    }

    return ERR_OK;
}

// ----------------------------------------------------------------------------
// Rebuild
// ----------------------------------------------------------------------------

RebuildCheck RebuildChecker::Check() const
{
    if (src_.empty() || dst_.empty()) {
        LOG(ERROR, "no source or dst in rebuild checker");
        return { ERR_OOPS, false };
    }

    struct timespec max_mt = { 0, 0 };
    struct stat st;
    bool first = true;

    for (auto const& src : src_) {
        auto path = src.ToString();
        int c = lstat(path.c_str(), &st);

        if (c != 0) {
            if (errno == ENOENT) {
                LOG(ERROR, "source not exists: %s", path.c_str());
                return { ERR_OOPS, false };
            }

            LOG(ERROR, "stat on `%s': %s", path.c_str(), strerror(errno));
            return { ERR_OOPS, false };
        }

        if (first) {
            first = false;
            max_mt = st.st_mtim;
        } else if (TimespecCmp(max_mt, st.st_mtim) < 0) {
            max_mt = st.st_mtim;
        }
    }

    for (auto const& dst : dst_) {
        auto path = dst.ToString();
        int c = lstat(path.c_str(), &st);

        if (c != 0) {
            // Need rebuild if target not exists.
            if (errno == ENOENT) {
                return { ERR_OK, true };
            }

            LOG(ERROR, "stat on `%s': %s", path.c_str(), strerror(errno));
            return { ERR_OOPS, false };
        }

        // Need rebuild if modified time of some source is larger.
        if (TimespecCmp(st.st_mtim, max_mt) < 0) {
            return { ERR_OK, true };
        }
    }

    return { ERR_OK, false };
}

static void TestStringTokenizer()
{
    StringTokenizer tokenizer("/usr/bin:/usr/local/bin:/home/Dale/.local/bin");

    auto token = tokenizer.Next(':');
    assert(token.ToString() == "/usr/bin");

    token = tokenizer.Next(':');
    assert(token.ToString() == "/usr/local/bin");

    token = tokenizer.Next(':');
    assert(token.ToString() == "/home/Dale/.local/bin");

    token = tokenizer.Next(':');
    assert(token.GetStart() == NULL);

    token = tokenizer.Next(':');
    assert(token.GetStart() == NULL);
}

Error RebuildNoScript(int argc, char** argv)
{
    LOG(DEBUG, "try to rebuild");

    RebuildChecker checker;

    checker.Src(Path() / "noscript.cpp")
        .Src(Path() / "libnoscript.cpp")
        .Src(Path() / "libnoscript.hpp")
        .Dst(Path() / "noscript");

    auto check = checker.Check();
    if (check.err != ERR_OK)
        return check.err;

    if (!check.need_rebuild) {
        LOG(DEBUG,
            "skip rebuild because of current noscript is up to date");
        return ERR_OK;
    }

    if (rename("noscript", "noscript.old") != 0) {
        LOG(ERROR,
            "failed to rename noscript to noscript.old: %s", strerror(errno));
        return ERR_OOPS;
    }

    CxxEnv env;

    CxxTarget cxx_target(&env, "noscript", CxxTarget::EXECUTABLE,
        CxxEnv::AUTO_LINK);

    Error err = cxx_target
                    .Src(Path() / "noscript.cpp")
                    .Src(Path() / "libnoscript.cpp")
                    .Build();

    if (err != ERR_OK) {
        // Restore the old builder if the new version failed to build.
        // rename("noscript.old", "noscript");
        return err;
    }

    // Move target path to ./noscript.
    rename(cxx_target.GetTargetPath().ToString().c_str(), "noscript");

    // Remove the backup if new version is ok.
    unlink("noscript.old");

    std::vector<char*> newargv;

    newargv.push_back(strdup("./noscript"));
    for (int i = 1; i < argc; ++i) {
        newargv.push_back(strdup(argv[i]));
    }
    newargv.push_back(NULL);

    LOG(INFO, "start new noscript");
    execvp("./noscript", newargv.data());

    LOG(ERROR, "execvp: %s", strerror(errno));
    return ERR_OOPS;
}

//
// Path
//

Error Path::Mkdir()
{
    if (components_.empty())
        return ERR_OK;

    std::string s = components_[0];
    for (size_t i = 1; i < components_.size(); ++i) {
        NOSCRIPT_TRY(MkdirAllowExists(s));

        s.push_back('/');
        s.append(components_[i]);
    }

    NOSCRIPT_TRY(MkdirAllowExists(s));

    return ERR_OK;
}

Path Path::Parent() const
{
    auto new_comp = components_;
    new_comp.pop_back();
    return Path { new_comp };
}

std::string Path::ToString() const
{
    if (components_.empty())
        return "";

    std::string s = components_[0];
    for (size_t i = 1; i < components_.size(); ++i) {
        s.push_back('/');
        s.append(components_[i]);
    }

    return s;
}

Path Path::JoinPath(Path const& path) const
{
    auto new_comp = components_;

    new_comp.reserve(path.components_.size() + new_comp.size());
    for (auto const& each : path.components_) {
        new_comp.push_back(each);
    }

    return Path { new_comp };
}

Path Path::Join(std::string component) const
{
    auto new_comp = components_;
    new_comp.emplace_back(std::move(component));
    return Path(new_comp);
}

Path Path::SetExtension(std::string const& name) const
{
    if (components_.empty())
        return *this;

    auto p = components_.back().rfind('.');
    if (p == std::string::npos)
        return *this;

    auto new_comp = components_;
    new_comp.back() = new_comp.back().substr(0, p) + name;

    return Path(new_comp);
}

// CxxEnv
//

static bool IsWhitespace(int c)
{
    switch (c) {
    case ' ':
    case '\r':
    case '\n':
    case '\t':
        return true;
    default:
        return false;
    }
}

static size_t SkipWs(std::string const& text, size_t offset)
{
    for (size_t i = offset; i < text.size(); ++i) {
        if (!IsWhitespace(text[i])) {
            return i;
        }
    }

    return std::string::npos;
}

static size_t UntilWs(std::string const& text, size_t offset)
{
    for (size_t i = offset; i < text.size(); ++i) {
        if (IsWhitespace(text[i])) {
            return i;
        }
    }

    return std::string::npos;
}

static void SplitWsInto(std::string const& text, std::vector<std::string>* out)
{
    size_t offset = 0;

    while (true) {
        offset = SkipWs(text, offset);
        if (offset == std::string::npos)
            break;

        size_t n = UntilWs(text, offset);

        if (n == std::string::npos) {
            out->push_back(text.substr(offset));
            break;
        }

        out->push_back(text.substr(offset, n - offset));
        offset = n;
    }
}

Error PkgConfig(CxxEnv* env, std::string const& package)
{
    auto cxxflags = noscript::Cmd("pkg-config")
                        .Arg("--cflags")
                        .Arg(package)
                        .GetOutput();

    SplitWsInto(cxxflags.child_stdout, &env->cxx_flags);

    auto ldflags = noscript::Cmd("pkg-config")
                       .Arg("--libs")
                       .Arg(package)
                       .GetOutput();

    SplitWsInto(ldflags.child_stdout, &env->ld_flags);

    return ERR_OK;
}

} // namespace noscript
